Linux Cpuidle介绍
一、引入背景
先来看一个抖音场景下面的功耗表现,这个是功耗分解板拆解出来的某一款手机cpu的核压和功率,会发现它们它们的数值不是一直保持在高位,有些时间会下降到很低。
这个时候有人会说下降是因为cpu上面没有任务在执行了,所以cpu就不需要工作造成功耗损失,那么就引出来一件事情,当cpu上面没有任务执行的时候,系统是如何进行cpu管理的,如果在没有任务的时候只是单纯的关闭cpu,那么下一次来一个事件的时候系统又如何兼顾此时的性能呢。在Linux kernel中,当cpu中没有任务在执行,也没有任何中断、异常信号过来的时候,我们称为处于idle状态,针对这种状态Linux设计了一套cpuidle framework框架,专门用于cpuidle的管理。
二、Idle状态
1. Idle状态,在代码里面是如何判断的呢
在Linux系统启动的时候,会在每个cpu上创建对应的idle进程,start_kernel()函数初始化内核需要的所有数据结构,并创建一个名为init的进程(pid=1),当init进程创建完后,cpu的idle进程处于cpu_idle_loop()无限循环中,当没有其他进程处于TASK_RUNNING状态时候,调度器才会执行cpu idle线程,让cpu进入idle模式.其函数调用关系简要概括如下:
start_kernel –> rest_init –> cpu_startup_entry,在cpu_startup_entry这个函数中,最终程序会进入无限循环do_idle loop中
在do_idle()中,代码会不断地轮询,判断当前系统是否需要调度,如果系统当前不需要调度,则进入到idle状态.
do_idle()->cpuidle_idle_call()->cpuidle_select()
在cpuidle_select函数里就是真正在进行cpu idle的选择操作。
2. 多级idle状态的产生
关闭一些核可以节省功耗,但关闭之后对时延(性能)必会造成一定的影响,如果在关闭之后很短的时间内就被唤醒,那么就会造成功耗/性能双方都不讨好,在进入退出idle的过程中也是会有功耗的损失的,如果在idle状态下面节省的功耗还无法弥补进入退出该idle的功耗,那么反而会得不偿失。根据性能/功耗的的这种矛盾,很多厂家会制定多个层级的idle状态,在每个层级下面的功耗、进入退出idle的功耗损失、以及进入退出的延迟都会是不同的数值,而cpuidle framework会根据不同的场景来进行仲裁选择使用何种的idle状态。
三、cpuidle framework软件架构
在kernel中cpuidle framework主体包含三个模块,分别为cpuidle core、cpuidle governors和cpuidle drivers,架构图如下:
cpu idle core:负责整体框架,同时负责和sched模块对接,当调度器发现没有任务在执行时候,就切换到idle进程,通知到cpuidle framework的cpuidle core模块要做接下来的idle操作。向cpuidle driver/governors模块提供统一的driver和governors注册和管理接口,向用户空间程序提供governor选择的接口。
cpuidle driver:负责具体idle机制的实现(不同等级下面idle的指标也是在这个模块进行填充),不同的平台会有不同的drivers实现。
cpudile governors:在这个模块进行cpuidle的选择,选择的算法主要是基于切换的功耗代价和系统的延迟容忍度,电源管理的目标就是在保证延迟在系统可以接受的范围内尽可能的节省功耗。
下面分别介绍下这几个模块的主体功能。
cpuidle core
cpuidle core是cpuidle framework的核心模块,负责抽象出cpuidle device、cpuidle driver和cpuidle governor三个实体。主体数据结构如下:
cpuidle_state:
上面也提到了现在很多厂家都会制定多个层级的idle状态,Linux kernel使用struct cpuidle_state结构抽象idle级别,主要成员的含义如下:
lname、desc:名称和简介;
lexit_latency:CPU从该idle state下返回运行状态的延迟
ltarget_residency: 期望的停留时间,进入退出idle状态是需要额外的功耗的,如果cpu很快进入退出idle状态,那么它的额外的功耗损失可能还弥补不了处于idle状态的功耗收益,从上面的图形中比较容易理解。所以就有了这样的一个指标,当cpu在idle状态下面停留超过一个的时间,才不会有功耗的损失,而这个临界点就是这里面提到的期望的停留时间,不同的平台和不同的idle等级下面这个值大小是不同的,Cpuidle Governor也会根据该数值来进行idle level的选择。
lpower_usage:cpu在该idle state下的功耗,单位nw
lenter:该state的回调函数
cpuidle_deivce
在现在的SMP系统中,每个cpu core都会有一个对应的cpuidle device,内核是通过使用struct cpuidle_device抽象cpuidle device,该结构体主要成员含义如下:
lenabled:设备是否已经使能
lcpu:该device对应的cpu number
llast_residency:该设备上一次停留在idle状态的时间
lstates_usage:记录了该设备的每个idle state的统计信息
cpuidle_driver:
主要成员含义如下:
lstates、state_count:该driver支持的cpuidle state及其个数
cpuidle driver的主要工作是定义所支持的cpuidle state,以及state的enter接口,如下面所示,cpudile driver就要负责将平台定义的idle-state信息填充到这个结构体中
cpuidle_governor:
内核通过使用struct cpuidle_governor结构体来抽象cpuidle governor,主要成员含义如下:
lgovernor_list:将该gover添加到一个全局的链表中
lrating:governor的级别
lenable/disable:governor的回调函数,主要进行一些初始化的操作
lselect:决策一个最佳的idle state
lreflect:告知governor上一次所处的idle state是哪一个
cpuidle governor是cpuidle框架进行idle state决策的核心,下面我们重点对该模块展开讲解。
四、cpuidle governor
在当前的内核中,有两种主流的governor策略:ladder和menu,选择哪一种取决于内核的配置,ladder在periodic timer tick system中使用,menu在tickless system中使用
Ladder,从字面上理解是阶梯式的策略,即要到更高的层级必须从低层级step by step上去,在ladder策略中,ladder governor会首先进入最浅的idle state,然后如果待的时间足够长,则会进入到更深一级的idle state,以此类推,直到到达最深的idle state,被唤醒时,会尽可能快地重新启动CPU;等到下次空闲,则又会从idle state1开始进入。在这种策略中,系统可能长时间都不进入最深的idle state中,造成功耗低一些损失。
Menu,从字面上理解是菜单式的策略,即只要具备进入更深层次idle state的条件,系统就可以选择进入到该idle state中,不需要从浅到深逐层递进。
由于主流系统中常采用tickless system,所以接下来重点介绍menu governor
在上面的章节中有提到,governor的主要职责是决策一个最佳的idle state(在kernel的术语中也成为c-state),主要的考量是基于切换的功耗代价和系统的延迟容忍度。
切换代价:
在讲cpuidle core的时候有提到cpuidle_state中有个成员变量,名为target_residency(期望的停留时间),这个可以认为是在一次切换过程中满足功耗不损失的min时间值,所以这里切换的代价落到实处就是需要在选择idle state的时候需要cpu在c state的停留时间要超过target_residency,这里的停留时间系统有一个术语来进行表征:预测时间(predicted_us)
系统的延迟容忍度:
系统延迟容忍度主要是考虑对性能的影响,系统延迟容忍度在前面的文章《Linux pm_qos介绍》中有提到,kernel会通过pm qos来获取当前系统对延迟的容忍度(latency_req),接下来governors所要做的事情就是在备选的几个c-state中,在所有exit_latency小于latency_req的state中,选取功耗(power_usage)最小的那个即可。
落到实处,这两个关键的考量点是如何来满足的呢:1、即预测时间如何来判断更准确,2、系统延迟容忍度如何更准确知道系统对延迟的要求。下面我们结合menu governor的主要结构和函数来展开。
主要数据结构:
Struct menu_governor:
在初始化的时候会调用cpuidle_register_governor来注册menu_governor,主要是提供了enable、select、reflect三个接口
struct menu_device:
lCorrection_factor:保存校正因子,一共有BUCKETS个(一般为12个),后面会提到
lBucket:当前使用的矫正因子
lNext_timer_ns:距离下一个tick来临的时间,在后面的代码中会提到。
主体函数接口:
menu_enable_device接口
该函数比较简单,主要任务就是初始化在结构体stuct menu_device中保存的校准因子correction_factor
menu_select接口
总结下来就是几个过程:
1、计算使用哪一类校正因子以及predicted_us,因为预测时间不是很准确的,期间随时都可能产生除next timer event之外的其它wakeup event。为了使预测更准确,有必要加入一个校正因子(correction factor),该校正因子基于过去的实际predicted_us和next_timer_us之间的比率,为了更精确,menu使用动态平均的factor。另外对于不同的next_timers_us,校正因子的影响程度是不一样的;对于不同的io wait场景,系统对校正因子也有着不同的敏感程度 。随后尝试通过最近的8个过去的停留时间来查找重复间隔,如果他们的标准差小于一定阈值,则将这8个时间的平均值作为predicted_us。最后取以上两个流程中的最小值
2、计算延迟容忍度:根据predicted_us和系统负荷(iowaiters)计算此时的延迟容忍度,计算公式为predicted_us / (1 +10 * iowaiters),这个公式反映的是退出延迟和预期停留时间之间的比例,iowaiters越大,对退出延迟的要求就越高,最后latency_req的值取上面这个估值和pm_qos 中lantency中的最小值作为最后的延迟容忍度
3、最后根据前面计算出来的两个因素来选取具体的idle state,将计算出的predicted_us与所有idle状态的停留时间进行比较,选择特定idle状态的条件是相应的停留时间应小于predicted_us。另外,将状态的exit_latency与系统的交互性要求进行比较。基于两个等待时间因素,选择适当的空闲状态。
在cpu退出idle状态后,menu governor会将将上一轮的进入idle状态的数据更新到menu driver中,作为下一次select的参数,具体见menu_reflect函数接口:
接下来下一次进入选择流程时,会先触发更新需求,在 menu_update函数接口中进行操作
以上是menu governor的核心流程,最终结果是在延迟容忍度满足的前提下选择一个最佳的idle state.
五、cpuidle案例呈现
在系统健康的情况下,cpuidle framwork会选择一个最适合性能功耗的c-state,但是在实际工作中,会遇到一些代码编写不规范的情况,导致最后的c-state处于一个我们不希望停留的状态。如下的一个案例:
在桌面idle场景,实际的trace表现:
而我们预期的表现应该是这样的
分析trace发现在idle的时候系统最深是进入到state=2状态,而目前主流的几个平台一般会存在c0~c4几个状态,idx为2是没有进入到此时预期的最深idle状态(此时为桌面待机场景,无其他操作,理应进入最深的idle状态)
添加debug信息发现request latency的时候有模块在request cpu_dma_latency=400us,而查看平台数据时发现该cpu有四个idle-state等级,exit-latency时间分别为:100us/250us/1200us/1400us,从而导致系统无法进入到更深层次的idle状态
六、总结
以上内容从背景、idle状态、cpuidle framework软件架构、cpuidle goveror四个维度进行cpuidle的介绍,并且以一个实际的案例来说明在工作中可能遇到的问题点,希望可以帮助到大家来学习linux cpuidle模块
参考文献:
1、http://www.wowotech.net/tag/cpuidle
2、https://www.kernel.org/doc/html/v5.0/admin-guide/pm/cpuidle.html
3、https://zhuanlan.zhihu.com/p/97996145
4、https://blog.csdn.net/feelabclihu/article/details/106866457